"""
    Defines 'getsmd' function that uses introspection to 
    generate SMD type datastructure describing an object api.
"""
import inspect
from logging import warning

# Using set() type for fastest membership testing, modifying  
# this set could break SMD operation (hence the privacy naming).
_primitives = set([str, int, float, long, unicode, list, tuple, bool])

def get_smd(service, public=True):
    """
        Get Simple Method Description (SMD) of object instance 
        'service'.  Include private method descriptors if 'public' 
        is present and False, otherwise only describe public API.
        
        @note: 'service' may be an instance or a class; 
        generated SMD is the same for both.
        
        @note: name 'service' is used to reflect service-centric 
        nature of SMDs.  Typically used to describe RPC services 
        to clients, which can then generate client-side proxy.
        
        @note: returned SMD structure may be JSON encoded to send 
        service description to JSON-RPC client for building proxy.   
        
        @return: SMD data structure describing service API.
    """
    # Mapping method-names to method-code objects.
    methods = inspect.getmembers(service, inspect.ismethod)
    if public:
        # Filter non-public (i.e., starting with "_") methods.
        methods = [(name,method) for name,method in 
                   methods if not name.startswith("_")]
    methods = [(name,inspect.getargspec(method)) for name,method in methods]
    descriptors = []
    for name,argspec in methods:
        args,varargs,kwargs,defaults = argspec
        if defaults:
            defaults = list(defaults)
        parameters = []
        for arg in reversed(args):
            if arg == "self":
                continue
            parameter = {"name": arg}
            if defaults:
                default = defaults.pop()
                # Only primitive default values are supported by 
                # SMD.  SMD clients are not expected to 
                # understand arbitrary object encodings.
                # Note use of 'type()' rather than isinstance: this 
                # is because there is no guarantee a primitive's 
                # subclass will "repr()" exactly  like a primitive, 
                # and the specialization would be lost if not.  One 
                # option would be to force the convert the value into 
                # an instance of its primitive superclass, but that's 
                # making too many assumptions for my taste. 
                if default is not None and type(default) not in _primitives:
                    message = ("SMD ignoring default value %r for"
                               " parameter %r of method %r on object %s."
                               "  Declaring parameter optional instead.")
                    warning(message % (default, arg, name, service))
                else:
                    parameter["default"] = default
                parameter["optional"] = True
            parameters.append(parameter)
        parameters.reverse()
        if varargs:            
            for arg in varargs:
                parameter = {"name": arg}
                parameter["optional"] = True
                parameters.append(parameter)
        descriptors.append({"name": name, "parameters": parameters})
    return descriptors
